/*  Sgvizler JavaScript SPARQL Google Visualization, version 0.2.0
 *  (c) 2011 Martin G. Skjæveland
 *
 *  Sgvizler is freely distributable under the terms of an MIT-style license.
 *  For details, see the Sgvizler web site: http://sws.ifi.uio.no/sgvizler/
 *
 *  Relies on Google Visualiztion API, jQuery.
 *  Requires SPARQL endpoint to return results sets on the format described
 *  in  http://www.w3.org/TR/rdf-sparql-json-res/.
 *--------------------------------------------------------------------------*/

var sgvizler = new function () {

    //// URLs:
    // Note same origin policy, see e.g. http://en.wikipedia.org/wiki/Same_origin_policy
    this.home = (window.location.href).replace(window.location.search, "");
    // *** The following are assumed set in the html file. Only this.endpoint is required.
    // *** this.endpoint     = "http://sws.ifi.uio.no/sparql/npd";
    // *** this.endpointQueryURL  = this.endpoint + "?output=text&amp;query=";
    // *** this.validatorQueryURL = "http://sws.ifi.uio.no/sparqler/validator?languageSyntax=SPARQL&amp;outputFormat=sparql&amp;linenumbers=true&amp;query=";
    
    //// Prefixes included in queries:
    this.namespaces = {
	'rdf' : "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
	'rdfs': "http://www.w3.org/2000/01/rdf-schema#",
	'owl' : "http://www.w3.org/2002/07/owl#",
	'xsd' : "http://www.w3.org/2001/XMLSchema#"
    };

    //// URL parameter names:
    this.paramQuery  = 'query';  // SPARQL query, prefixes defined in this.namespaces not neccessary. 
    this.paramChart  = 'chart';  // chart type
    this.paramWidth  = 'width';  // width of the chart
    this.paramHeight = 'height'; // height of the chart
    this.paramLog    = 'log';    // log level: 0, 1, 2
    this.params = [ this.paramQuery, this.paramChart, this.paramWidth, this.paramHeight, this.paramLog ];
    this.divAttrPrefix = 'data-sgvizler-';  // the div equivalents have a prefix
    
    //// URL parameter default values:
    this.defaults = {};
    this.defaults[this.paramQuery]  = "SELECT ?class (count(?instance) AS ?noOfInstances)\nWHERE{ ?instance a ?class }\nGROUP BY ?class\nORDER BY ?class", 
    this.defaults[this.paramWidth]  = '800';
    this.defaults[this.paramHeight] = '400';
    this.defaults[this.paramChart]  = 'LineChart'; 
    this.defaults[this.paramLog]    = 2;

    //this.gvizChartTypes; //set by init()

    //// google chart draw() options:
    this.gvizChartArea = {left:"5%", top:"5%", width:"75%", height:"80%"};
    this.gvizGeoRegion = '154'; // region to display
    this.gvizGeoColors = [0xFF8747, 0xFFB581, 0xc06000];
    this.gvizGeoMapDataMode = 'markers';
    this.gvizGeoChartBackgroundColor = 'red'; // TODO

    //// #id's to html elements:
    // (Note also that the functions this.setGChartType and this.setGChartOptions 
    // reference html elements, the options in this.formChart.)
    this.chartCon     = 'sgvzlr_gchart';    // #id to the div to hold the chart
    this.queryForm    = 'sgvzlr_formQuery'; // 
    this.queryTxt     = 'sgvzlr_cQuery';    // query text area.
    this.formQuery    = 'sgvzlr_strQuery';  // hidden query string. "trick" taken from snorql.
    this.formWidth    = 'sgvzlr_strWidth';  // 
    this.formHeight   = 'sgvzlr_strHeight'; // 
    this.formChart    = 'sgvzlr_optChart';  // 
    this.prefixCon    = 'sgvzlr_cPrefix';   // print prefixes
    this.messageCon   = 'sgvzlr_cMessage';  // print messages
    //this.JSONCon    = 'sgvzlr_cJSON';     // 
    
    
    ///////////////////////////////////////////////////////////////////////////
    ///////////////////////////////////////////////////////////////////////////

    this.init = function(){
	sgvizler.gvizChartTypes = [
	    { 'value': "LineChart",        'name': "Line Chart",        'function': google.visualization.LineChart }, 
	    { 'value': "AreaChart",        'name': "Area Chart",        'function': google.visualization.AreaChart }, 
	    { 'value': "PieChart",         'name': "Pie Chart",         'function': google.visualization.PieChart }, 
	    { 'value': "ColumnChart",      'name': "Column Chart",      'function': google.visualization.ColumnChart },
	    { 'value': "BarChart",         'name': "Bar Chart",         'function': google.visualization.BarChart }, 
	    { 'value': "ScatterChart",     'name': "Scatter Chart",     'function': google.visualization.ScatterChart }, 
	    { 'value': "CandlestickChart", 'name': "Candlestick Chart", 'function': google.visualization.CandlestickChart }, 
	    { 'value': "OrgChart",         'name': "Org Chart",         'function': google.visualization.OrgChart }, 
	    { 'value': "TreeMap",          'name': "Tree Map",          'function': google.visualization.TreeMap }, 
	    { 'value': "Timeline",         'name': "Timeline",          'function': google.visualization.AnnotatedTimeLine }, 
	    { 'value': "MotionChart",      'name': "Motion Chart",      'function': google.visualization.MotionChart }, 
	    { 'value': "GeoChart",         'name': "Geo Chart",         'function': google.visualization.GeoChart }, 
	    { 'value': "GeoMap",           'name': "Geo Map",           'function': google.visualization.GeoMap }, 
	    { 'value': "Map",              'name': "Map",               'function': google.visualization.Map },
	    { 'value': "Table",            'name': "Table",             'function': google.visualization.Table }
	];

	// populate drop-down box:
	for (var i = 0, l = sgvizler.gvizChartTypes.length; i < l; i++) {
	    $('#' + sgvizler.formChart)
		.append($('<option/>')
			.val(sgvizler.gvizChartTypes[i]['value'])
			.html(sgvizler.gvizChartTypes[i]['name']));
	}
    }


    this.go = function(){
	sgvizler.init(); // wait until google libraries are loaded.

	var p = sgvizler.getUrlParams(),
	    u = sgvizler.setUserInput(p);

	if(! $(sgvizler.prefixCon).length)
	    sgvizler.displayPrefixes();
	if(! $(sgvizler.formQuery).length)
	    sgvizler.displayUserInput(u);
	if(! $(sgvizler.chartCon).length && p[sgvizler.paramQuery]) // query supplied in URL
	    sgvizler.displayChart(sgvizler.chartCon, u);
		
	sgvizler.displayDivQueries();
    }

    this.displayDivQueries = function(){
	$('div[' + sgvizler.divAttrPrefix + sgvizler.paramQuery + ']').each(function() {
	    var id = $(this).attr('id'),
	    u = {};
	    for (var i = 0, l = sgvizler.params.length; i < l; i++) {
		u[sgvizler.params[i]] = $(this).attr(sgvizler.divAttrPrefix + sgvizler.params[i]);
	    }
	    // width and height are set in css:
	    u[sgvizler.paramWidth] = $(this).css("width");
	    u[sgvizler.paramHeight] = $(this).css("height");
	    u = sgvizler.setUserInput(u);
	    sgvizler.displayChart(id, u);
	});
    }
	
    this.displayChart = function (div, user){
	var chart = sgvizler.getGChartType(div, user[sgvizler.paramChart]),
	    opt = sgvizler.getGChartOptions(user);
	google.visualization.events.addListener(chart, 'error', function(err){
	    sgvizler.displayMessage(div, 6, {'log': log, 'error': err.message}); }); 
	sgvizler.drawGChart(div, user[sgvizler.paramQuery], user[sgvizler.paramLog], function(data){
	    var datatable = new google.visualization.DataTable(data);
	    chart.draw(datatable, opt); });
    }
 
    this.displayUserInput = function(userdata){
	$('#'+sgvizler.queryTxt).val(userdata[sgvizler.paramQuery]);	
	$('#'+sgvizler.formWidth).val(userdata[sgvizler.paramWidth]);
	$('#'+sgvizler.formHeight).val(userdata[sgvizler.paramHeight]);
	$('#'+sgvizler.formChart).val(userdata[sgvizler.paramChart]);
    }
    
    this.setUserInput = function(input){
	var userdata = {};
	for (var i = 0, l = sgvizler.params.length; i < l; i++) {
	    var param = sgvizler.params[i];
	    if(typeof input[param] == 'undefined' || ! input[param].length){
		userdata[param] = sgvizler.defaults[param];
	    } else {
		userdata[param] = input[param];
	    }
	}
	return userdata;
    }
	    
    this.displayMessage = function (div, messageNo, vars){
	var message = "";
	if(vars['log'] == 0){
	    return;
	} else if(vars['log'] == 1){
	    if(messageNo == 1){
		message = "Loading...";
	    } else if(messageNo == 2 || messageNo == 3 || messageNo == 6){
		message = "Error.";
	    }
	} else{
	    if(messageNo == 1){
		message = "<p>Sending query...</p>";
	    } else if(messageNo == 2){
		message = "<p>Error querying endpoint. Possible errors:<ul>" +
		    "<li>Malformed SPARQL query";
		if(typeof sgvizler.validatorQueryURL != 'undefined'){
		    message += ", <a href='" + sgvizler.validatorQueryURL + vars['query'] +
			"'>check if it validates</a>"; }
		message += ".</li><li><a href='" + sgvizler.endpoint + "'>SPARQL endpoint</a> down";
		if(typeof sgvizler.endpointQueryURL != 'undefined'){
		    message += ", check if <a href='" + sgvizler.endpointQueryURL  + vars['query'] + "'>" +
			"query runs at endpoint</a>";}
		message += ".</li>";
		message += "<li>Same origin policy violation, is this page <code>" + 
		    sgvizler.home + "</code> and the SPARQL endpoint <code>" + sgvizler.endpoint + 
		    "</code> located at the same domain and port?</li></ul></p>";
	    } else if(messageNo == 3){
		message = "<p>Unknown error.</p>";
	    } else if(messageNo == 4){
		message = "<p>Query returned no results.</p>"
	    } else if(messageNo == 5){
		message = "<p>Received " + vars['noRows'] + " rows. Drawing chart...";
		if(typeof sgvizler.endpointQueryURL != 'undefined'){
		    message += "<br/><a target='_blank' href='" + sgvizler.endpointQueryURL  + vars['query'] + "'>" +
			"View query results</a> (in new window).";}
		message += "</p>";
	    } else if(messageNo == 6){
		message = "<p>Error drawing chart: " + vars['error'] + "</p>";
	    }
	}

	if(div == sgvizler.messageCon && sgvizler.messageCon.length){
	    $('#'+sgvizler.messageCon).html(message);
	} else{
	    $('#'+div).html(message);
	}
    }

    this.displayPrefixes = function (){
	$('#'+sgvizler.prefixCon).text(sgvizler.getPrefixes());
    }

    this.resetPage = function() {
        document.location = sgvizler.home;
    }

    this.submitQuery = function() {
	$('#'+sgvizler.formQuery).val($('#'+sgvizler.queryTxt).val());
        $('#'+sgvizler.queryForm).submit();
    }

    this.getPrefixes = function() {
        var prefixes = "";
        for (prefix in sgvizler.namespaces) {
            prefixes += 'PREFIX ' + prefix + ': <' + sgvizler.namespaces[prefix] + '>\n';
        }
        return prefixes;
    }    

    this.getUrlParams = function () {
	var urlParams = {},
	    e,
            a = /\+/g,  // regex replacing '+' with a space
            r = /([^&=]+)=?([^&]*)/g,
            d = function (s) { return decodeURIComponent(s.replace(a, " ")); },
            q = window.location.search.substring(1);
	
	while (e = r.exec(q))
	    if(e[2].length > 0)
		urlParams[d(e[1])] = d(e[2]);
	return urlParams;
    }

    this.getGChartType = function(divId, charttype){
	var div = document.getElementById(divId);
	for (var i = 0, l = sgvizler.gvizChartTypes.length; i < l; i++) {
	    if(charttype == sgvizler.gvizChartTypes[i]['value'])
		return new sgvizler.gvizChartTypes[i]['function'](div);
	    }
	return new sgvizler.gvizChartTypes[0]['function'](div); // first is default.
    }

    this.getGChartOptions = function(u){
	var options = {'width'  : u[sgvizler.paramWidth], 
		       'height' : u[sgvizler.paramHeight], 
		       chartArea: sgvizler.gvizChartArea
		      };

	if(u[sgvizler.paramChart] == 'geomap' || u[sgvizler.paramChart] == 'geochart'){
	    options['dataMode'] = sgvizler.gvizGeoMapDataMode;
	    options['region'] = sgvizler.gvizGeoRegion;
	    options['colors'] = sgvizler.gvizGeoColors;
	    options['backgroundColor'] = sgvizler.gvizGeoChartBackgroundColor;
	}
	else if(u[sgvizler.paramChart] == 'tree'){
	    sgvizler.setChartContainerOptions(u); // TODO, does this work?
	}
	else if(u[sgvizler.paramChart] == 'map'){
	    options['showTip'] = true;
	    options['useMapTypeControl'] = true;
	    sgvizler.setChartContainerOptions(u);
	}
	return options;
    }

    this.setChartContainerOptions = function(u){
	$('#'+sgvizler.chartCon).css('width', u[sgvizler.paramWidth]);
	$('#'+sgvizler.chartCon).css('height', u[sgvizler.paramHeight]);
    }

    this.drawGChart = function(div, query, log, callback){
	sgvizler.getQueryResults(query, sgvizler.parseJSONSparql2Google, div, log, callback);
    }
    
    this.getQueryResults = function(query, resultparser, div, log, callback){
	sgvizler.displayMessage(div, 1, {'log': log });
	var encodedQuery = encodeURIComponent(sgvizler.getPrefixes() + query);
	$.getJSON(sgvizler.endpoint,
		       {query: sgvizler.getPrefixes() + query, output: "json"})
	    .error(function() { 
		sgvizler.displayMessage(div, 2, {'log': log, 'query': encodedQuery});
	    })
	    .success(function(data) {
		    $.parseJSON(data);
		    var noRows = sgvizler.countRowsJSONSparql(data)
		    if(noRows == null){   
			sgvizler.displayMessage(div, 3, {'log': log });
		    } else if(noRows == 0){ 
			sgvizler.displayMessage(div, 4, {'log': log });
		    } else{
			sgvizler.displayMessage(div, 5, {'log': log, 'noRows': noRows});
			data = resultparser(data);
			callback(data);
		    }
		})
	//.complete(function() { })
	; // end .getJSON
    }
    

    //// JSON parser: SPARQL JSON to "GOOGLE JSON".
    // variable notation: xtable, xcol(s), xrow(s) -- x is 's'(parql) or 'g'(oogle).

    this.countRowsJSONSparql = function(stable){
	if(typeof (stable["results"])["bindings"] != 'undefined'){
	    return (stable["results"])["bindings"].length;
	} else return null;
    }
    
    this.parseJSONSparql2Google = function(stable){
	var gcols = [],
	    grows = [],
	    gdatatype = [], // for easy reference of datatypes
     	    scols = (stable["head"])["vars"],
	    srows = (stable["results"])["bindings"];

	for (var c = 0, l = scols.length; c < l; c++) {
	    gdatatype[c] = sgvizler.getGoogleJsonDatatype(
		(((stable["results"])["bindings"])[0])[scols[c]]); // NB! 
	    gcols[c] = {"id": scols[c], "label": scols[c], "type": gdatatype[c]};
	}

	// loop rows
	for (var r = 0, lr = srows.length; r < lr; r++) {
	    var srow = srows[r],
	        grow = [];
	    // loop cells
	    for (var c = 0, lc = scols.length; c < lc; c++) {
		var value = sgvizler.getGoogleJsonValue(
		    (srow[scols[c]])['value'], gdatatype[c]);
		grow[c] = {"v": value};
	    }
	    grows[r] = {"c": grow};
	}
	return {"cols": gcols, "rows": grows};
    }

    this.getGoogleJsonValue = function(value, datatype){
	if(datatype == 'number'){
	    return Number(value);
	} else if(datatype == 'date'){
	    //assume format yyyy-MM-dd
	    return new Date(value.substr(0,4), 
			    value.substr(5,2), 
			    value.substr(8,2));
	} else if(datatype == 'datetime'){
	    //assume format yyyy-MM-ddZHH:mm:ss
	    return new Date(value.substr(0,4), 
			    value.substr(5,2), 
			    value.substr(8,2),
			    value.substr(11,2),
			    value.substr(14,2),
			    value.substr(17,2));
	} else if(datatype == 'timeofday'){
	    //assume format HH:mm:ss
	    return new Array(value.substr(0,2), 
			     value.substr(3,2), 
			     value.substr(6,2));
	} else { // datatype == 'string' || datatype == 'boolean'
	    return value;
	}
    }

    this.getGoogleJsonDatatype = function(scell){
	if(scell['type'] == 'typed-literal'){
	    if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#float' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#double' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#decimal' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#int' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#long' ||
	       scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#integer'){
		return 'number';
	    } else if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#boolean'){
		return 'boolean';
	    } else if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#date'){
		return 'date';
	    } else if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#dateTime'){
		return 'datetime';
	    } else if(scell['datatype'] == 'http://www.w3.org/2001/XMLSchema#time'){
		return 'timeofday';
	    }
	} else {
	    return 'string';
	}
    }
}